<?php
namespace DRY\Record;

/**
 * DonReY Framework 2012 :: Record module - "Single" record type
 * One table contains all fields in this record
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/Record/single.html
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module.Record
 */
class Type_Single extends base
{
	/**
	 * Current state of the record:
	 * 'init' - Record object was just initialized, it doesn't contain any data (ID not known)
	 * 'create' - New data was assigned to the record (import or assign), without ID being known (ID not known)
	 * 'idset' - Record received it's ID, but still no data (not read, not new) (ID known)
	 * 'sync' - Record's content was just read, $_data is the same as database data, or just written to db (ID known)
	 * 'readfail' - Last read operation failed, because there was no corresponding row in database (ID known)
	 * 'preset' - Data was assigned to the record (import or assign), along with an ID, without the record being read (yet). It is possible that the record already has some data (ID knwon)
	 * 'update' - There was a read, but the data was changed (import or assign) (ID known)
	 * 'delete' - Record has been deleted (solo) or is being scheduled for deletion (set). This mode is final, all other operations will refuse once record is deleted
	 * @var string
	 */
	protected $_state = 'init';

	/**
	 * "Single" type record constructor - Because for this record type the fields are known, initialize the $_data array with nulls
	 * @param \DRY\Module\Record $rec
	 * @param string $model
	 * @param mixed $id
	 * @param bool $set Mode of operation ( Solo or Set )
	 */
	public function __construct(\DRY\Module\Record $rec, $model, $id=null, $set = false)
	{
		parent::__construct($rec, $model, $id, $set);
		// "Single" record type doesn't function if field names have not been specified!
		if(!isset($this->_def['fields']) || empty($this->_def['fields']))
			throw new \DRY\Water('record', \DRY\Module\Record::EXC_FIELDS_NOT_DEFINED);

		foreach(array_keys($this->_def['fields']) as $field)
			$this->_data[$field] = null;
	}

	/**
	 * Loads a database result row into the record's $_data storage. Does conversions if they were configured
	 * Only loads the existing fields, ignoring everythign else.
	 * @param array $raw_data Database result row ( from fetch_assoc() or similar )
	 * @param bool $overwrite True = overwrite data in fields that are non-null, False = keep existing data dismissing the database ones
	 * @return string New possible state of record (depending if data was merged or not)
	 */
	protected function load_Raw($raw_data, $overwrite = false)
	{
		if(empty($raw_data))
			return 'readfail';
		// set the ID if it's present in $raw_data
		if(isset($raw_data[$this->_def['id_field']]))
			$this->_id = $raw_data[$this->_def['id_field']];

		// goes through all field names and import the data
		foreach(array_keys($this->_def['fields']) as $field) {
			if($overwrite || is_null($this->_data[$field])) {
				if(isset($raw_data[$field]))
					$this->_data[$field] = $this-> convert_Raw($field, $raw_data[$field]);
			} else {
				// when merging, if the data existing in the record is identical to the preset data, remove the field from the list of those that need updating
				if(isset($raw_data[$field]) && $this-> convert_Raw($field, $raw_data[$field])==$this->_data[$field])
					unset($this->_updated_fields[$field]);
			}
		}
		if($overwrite || empty($this->_updated_fields))
			return 'sync';
		else
			return 'update';
	}

	/**
	 * Constructs the data which is ready for sending to the database out of the Record's internal storage
	 * @param array|null $which_fields If specified, only uses these fields, ignoring the rest
	 * @param bool $add_id True = adds the existing record ID to the returned data
	 * @return array
	 */
	protected function prepare_Raw($which_fields = null, $add_id = false)
	{
		$raw_data = array();

		if(is_null($which_fields) || empty($which_fields))
			$which_fields = array_keys($this->_def['fields']);
		// goes through the field names and export the data
		foreach($which_fields as $field) {
			if(!is_null($this->_data[$field]))
				$raw_data[$field] = $this-> convert_Internal($field);
		}
		// add ID to the data if requested (and not null)
		if($add_id && !is_null($this->_id))
			$raw_data[$this->_def['id_field']] = (string)$this->_id;
		return $raw_data;
	}

//===== Field access methods =====//

	/**
	 * @see DRY_Record_base::__get()
	 */
	public function __get($field)
	{
		if($field == 'id')
			return $this-> get_ID();

		if(!isset($this->_def['fields'][$field]))
			return null; // field doesn't exist

		switch($this->_state) {
			case 'init':
			case 'delete':
				// no data has been set yet, or the data doesn't exist anymore
				return null;
			case 'idset':
			case 'preset':
				// ID was set, but the record hasn't yet been read, so attempt to read it before accessing field
				$this-> read();
				break;
		}

		// in other cases, return the field's value (existing or default (from def))
		return $this->_data[$field];
	}

	/**
	 * Attempts to retrieve the ID of this record
	 * @return int|string Content of the ID field, or null if there is none
	 */
	public function get_ID()
	{
		switch($this->_state) {
			case 'init':
			case 'delete':
			case 'readfail':
				// no ID
				return null;
			case 'create':
				// no ID yet, but there is some data, so attempt to create the record by writing it
				$this-> write(true);
				return $this->_id;
				// we have an ID set, but database hasn't been checked yet, attempt to read
			case 'idset':
				$this-> read(true);
				if($this->_state=='readfail')
					return null;
				else
					return $this->_id;
			default:
				// in other cases, there is an ID
				return $this->_id;
		}
	}

	/**
	 * List with fields that have been changed from their "synced" values
	 * Useful for write() when updating the record
	 * @var array
	 */
	protected $_updated_fields = array();

	/**
	 * @see DRY_Record_base::__set()
	 */
	public function __set($field, $value)
	{
		if($field == 'id')
			return $this-> set_ID($value);

		if(!isset($this->_def['fields'][$field]))
			return; // field doesn't exist

		switch($this->_state) {
			case 'init':
				// no ID yet, so begin with create mode
				if($value!=null)
					$this->_state = 'create';
				break;
			case 'idset':
				// a value is added when an ID exists, but the record hasn't yet been synced
				if($value!=null)
					$this->_state = 'preset';
			case 'create':
				// no change in state here
				break;
			case 'sync':
			case 'update':
				if($value!=$this->_data[$field]) {
					$this->_state = 'update';
					$this->_updated_fields[$field] = true;
				}
				break;
			case 'delete':
				throw new \DRY\Water('record', \DRY\Module\Record::EXC_RECORD_ALREADY_DESTROYED);
		}
		// after state has been changed, just put the value in the data storage
		$this->_data[$field] = $value;

		// preset state means that the fields that have been introduced will get updated in the existing record in case the state doesn't change
		if($this->_state == 'preset')
			$this->_updated_fields[$field] = true;
	}

	/**
	 * Sets the record's ID if it didn't previously exist
	 * @param int|string $id
	 * @param bool $force Forcibly changes or sets the ID. DON'T USE! Internal for RecordSet implementation
	 */
	public function set_ID($id, $force = false)
	{
		if(!$force)
		switch($this->_state) {
			case 'init':
			case 'idset':
				// if ID is set after init, change state to "idset" to indicate that the record is ready to be read (or merged)
				$this->_state = 'idset';
				break;
			case 'create':
				// An ID is given when there's already some stored data - Reads will now attempt to merge ( unless forced to overwrite )
				$this->_state = 'preset';
				break;
			case 'delete':
				throw new \DRY\Water('record', \DRY\Module\Record::EXC_RECORD_ALREADY_DESTROYED);
			default:
				// in all other cases, the call is ignored because ID cannot be changed once it's been set for the same object
				return false;
		}
		$this->_id = $id;
	}

//===== Database operations, Solo mode =====//

	/**
	 * This record type performs a write operation when being destructed, if working on Solo mode
	 */
	public function __destruct()
	{
		$this-> write(); // write if necessary, not forced
	}

	/**
	 * Reads the record's corresponding row in the database, if necessary
	 * @see DRY_Record_base::read()
	 * @throws Water
	 */
	public function read($overwrite=false)
	{
		// this method is for Solo mode.
		if($this->_setMode == true) return;

		switch($this->_state) {
			case 'init':
			case 'create':
				// error, cannot read when the ID is not known
			case 'readfail':
				// last read operation failed, don't attempt again ( even if ID is known )
				return false;
			case 'sync':
			case 'update':
				// ID known, record is already synced, or was read then updated, do the read only if $overwrite flag is set
				if(!$overwrite)
					return true;
			case 'idset':
			case 'preset':
				// ID known, proceed to reading the record
				$q = $this->_rec['queryfactory']-> select()
					-> table($this->_def['table'])
					-> limit(1)
					-> where($this->_def['id_field'].' = ?', $this->_id);

				$this->_state = $this-> load_Raw($this->_rec['connection']-> query($q)-> current(), $overwrite);
				return true;
			case 'delete':
				throw new \DRY\Water('record', \DRY\Module\Record::EXC_RECORD_ALREADY_DESTROYED);
			default:
				// fatal error if the record is not in one of the existing "states"
				throw new \DRY\Water('record', \DRY\Module\Record::EXC_INVALID_RECORD_STATE);
		}
	}

	/**
	 * Alternate method of populating the record with data, by searching for arbitrary conditions.
	 * It is irrelevant if the record had previously any data, if it has ID or not, it is completely overwritten
	 * @param callback $callback Callback to a method that receives the QueryFactory object, to put arbitrary conditions.
	 */
	public function search($callback)
	{
		// this method is for Solo mode.
		if($this->_setMode == true) return;

		if($this->_state == 'delete')
			throw new \DRY\Water('record', \DRY\Module\Record::EXC_RECORD_ALREADY_DESTROYED);

		if(is_callable($callback)) {
			$q = $this->_rec['queryfactory']-> select()
				-> table($this->_def['table'])
				-> limit(1);
			// use the callback to add aditional filters (where, order, perhaps joins?) to this read
			$callback($q);
			$this->_state = $this-> load_Raw($this->_rec['connection']-> query($q)-> current(), true); // TODO: ??? must think about $state after failed search()
			return true;
		} else
			return false;
	}

	/**
	 * Intelligent write operation (create, update, or just ignore), depending on the record's state
	 * Note:After a write operation, the record MUST have an ID, if there was a fatal error in an Insert operation and the ID cannot be created, throw an exception
	 * @see DRY_Record_base::write()
	 */
	public function write($force = false)
	{
		// this method is for Solo mode.
		if($this->_setMode == true) return;

		switch($this->_state) {
			case 'init':
				// nothing happened yet to the record, doesn't attempt to create an empty record (unless forced)
				if(!$force)
					return false;
			case 'readfail':
				// do not attempt to write if a read has failed (empty record) and there is no ID
			case 'idset':
				// do not attempt to write if the record just received it's id, but no other action has been done
				if(!$force || is_null($this->_id))
					return false;
			case 'create':
				// attempts to create the record with whatever values are stored ( no ID ) - Insert operation
				$write_mode = "insert";
				break;
			case 'sync':
				// when synced, doesn't attempt to write unless forced
				if(!$force)
					return false;
			case 'update':
			case 'preset':
				// in the other cases (sync, update, preset), attempt to update an existing record with the stored values - if that fails go back to insert mode
				$write_mode = "update";
				break;
			case 'delete':
				// nothing to write an an already deleted record, ignore the call
				return false;
			default:
				// fatal error if the record is not in one of the existing "states"
				throw new \DRY\Water('record', \DRY\Module\Record::EXC_INVALID_RECORD_STATE);
		}
		$raw_data = $this-> prepare_Raw($write_mode == 'update'?array_keys($this->_updated_fields):null, $write_mode == 'insert');

		// try Update mode first (ID known)
		if($write_mode == 'update') {
			$q = $this->_rec['queryfactory']-> update()
				-> table($this->_def['table'])
				-> values($raw_data)
				-> where($this->_def['id_field'].' = ?', $this->_id);
			$this->_rec['connection']-> query($q);
			// see what happened
			$sql_response = $this->_rec['connection']-> get_Response();
			if($sql_response['rows_matched']==0)
				$write_mode = 'insert'; // oops, update failed, possibly because the row doesn't yet exist, so try to insert
		}

		if($write_mode == 'insert') {
			$q = $this->_rec['queryfactory']-> insert()
				-> table($this->_def['table'])
				-> values($raw_data);
				$this->_rec['connection']-> query($q);

			// if an ID didn't previously exist, make one now (records of this type MUST have AUTO_INCREMENT set:
			if(is_null($this->_id)) {
				// critical error if the table doesn't return an insert ID, since we can't know the ID of the just inserted record
				if($this->_rec['connection']-> insert_id == 0)
					throw new \DRY\Water('record', \DRY\Module\Record::EXC_INSERT_ID_NOT_RETURNED);
				$this->_id = $this->_rec['connection']-> insert_id;
			}
		}

		// we're done, change state
		$this->_state = 'sync';
		return true;
	}

	/**
	 * @see DRY_Record_base::destroy()
	 */
	public function destroy()
	{
		switch($this->_state) {
			case 'init':
			case 'create':
			case 'delete':
				// nothing special to do, record doesn't exist ( or not anymore )
				$this->_state = 'delete';
				return false;
			default:
				$this->_state = 'delete';
				// in all other cases, if working in solo mode, remove the record.
				if($this->_setMode == false) {
					$q = $this->_rec['queryfactory']-> delete()
						->table($this->_def['table'])
						->where($this->_def['id_field'].' = ?', $this->_id);
					$this->_rec['connection']-> query($q);
					return $this->_rec['connection']-> affected_rows > 0;
				} else
					return true;
		}
	}

//===== Database operations, Set mode =====//

	/**
	 * Yes, "Single" record does implement Join-based multi-row reads
	 * @see DRY_Record_base::_implements_Join()
	 */
	public static function _implements_Join()
	{
		return true;
	}

	public static function _attach_Tables(\DRY\Queryfactory\Dialect_base $q, $def, $mode = 'select')
	{
		switch($mode) {
			case 'insert':
				$fields = array_keys($def['fields']);
				break;
			default:
				$fields = null;
		}
		$q-> table($def['table'], $fields, 'left', "#1.id = {$def['table']}.id");
	}

	public function _prepare_WriteContent()
	{
		// this method is for Set mode.
		if($this->_setMode == false) return;

		switch($this->_state) {
			case 'init':
				// nothing happened yet to the record, nothing to write
			case 'sync':
				// when synced, doesn't attempt to write anything
				return false;
//			case 'readfail': // no 'readfail' mode possible in Set mode, as "read" method is not available
			case 'create':
			case 'idset':
				// attempts to create the record with whatever values are stored ( no ID ) - Insert operation
				$write_mode = "insert";
				break;
			case 'update':
			case 'preset':
				// in the other cases (sync, update, preset), attempt to update an existing record with the stored values - if that fails go back to insert mode
				$write_mode = "update";
				break;
			case 'delete':
				// just return the ID so RecordSet knows that this one should be deleted
				return array(
					'mode' => "delete",
					'id' => $this->_id
				);
			default:
				// fatal error if the record is not in one of the existing "states"
				throw new \DRY\Water('record', \DRY\Module\Record::EXC_INVALID_RECORD_STATE);
		}

		// assumes everything will go fine in the group write?
		$this->_state = 'sync';
		return array(
			'mode' => $write_mode,
			'id' => $this->_id,
			'data' => $this-> prepare_Raw($write_mode == 'update'?array_keys($this->_updated_fields):null, $write_mode == 'insert')
		);
	}

	/**
	 * Loads a row from a database result which can be used as record "raw" data. Normally used by RecordSet, in _setMode, but can come from external source as well
	 * @param array $row
	 * @param bool $override To allow loading even if not working in "Set" mode
	 */
	public function _load_SetRow($row, $override = false)
	{
		// this method is for Set mode.
		if(!$override && $this->_setMode == false) return;

		$this-> load_Raw($row);
		$this->_state = 'sync';
	}
}